﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace SharpShare.Utilities {
    public class SeekableStream : Stream {
        private object _synclock = new object();
        private long _totalLength;
        private long _position;

        public SeekableStream(Stream baseStream, Stream seekableBackingStream, long totalLength = 0) {
            if (baseStream == null || seekableBackingStream == null) {
                throw new ArgumentNullException();
            }

            if (!seekableBackingStream.CanSeek) {
                throw new ArgumentException("Backing stream must be seekable.", "seekableBackingStream");
            }

            this.BaseStream = baseStream;
            this.BackingStream = seekableBackingStream;

            _totalLength = totalLength;
        }

        public Stream BaseStream { get; private set; }
        public Stream BackingStream { get; private set; }

        public override bool CanRead {
            get { return this.BaseStream.CanRead; }
        }

        public override bool CanSeek {
            get { return true; }
        }

        public override bool CanWrite {
            get { return false; }
        }

        public override void Flush() {
            throw new NotSupportedException();
        }

        public override long Length {
            get {
                if (_totalLength != 0) {
                    return _totalLength;
                }

                EnsureRangeAvailable(int.MaxValue, 1);

                return this.BaseStream.Length;
            }
        }

        public override long Position {
            get {
                return _position;
            }
            set {
                _position = value;
            }
        }

        public override int Read(byte[] buffer, int offset, int count) {
            lock (_synclock) {
                EnsureRangeAvailable(this.Position, count);
                this.BackingStream.Position = _position;
                int read = this.BackingStream.Read(buffer, offset, count);
                _position += read;
                return read;
            }
        }

        public override long Seek(long offset, SeekOrigin origin) {
            switch (origin) {
                case SeekOrigin.Begin:
                    _position = offset;
                    break;
                case SeekOrigin.End:
                    _position = (this.BaseStream.Length - offset);
                    break;
                case SeekOrigin.Current:
                    _position += offset;
                    break;
            }

            return _position;
        }

        public override void SetLength(long value) {
            throw new NotSupportedException();
        }

        public override void Write(byte[] buffer, int offset, int count) {
            throw new NotSupportedException();
        }

        public override void Close() {
            base.Close();
            this.BaseStream.Close();
            this.BackingStream.Close();
        }

        private void EnsureRangeAvailable(long offset, int count) {
            lock (_synclock) {
                while (this.BackingStream.Length < (offset + count)) {
                    long bytesNeeded = ((offset + count) - this.BackingStream.Length);
                    byte[] buffer = new byte[bytesNeeded];
                    int bytesGot = this.BaseStream.Read(buffer, 0, buffer.Length);
                    if (bytesGot == 0) {
                        break;
                    }

                    this.BackingStream.Seek(0, SeekOrigin.End);
                    this.BackingStream.Write(buffer, 0, bytesGot);
                }
            }
        }
    }
}
